/* Copyright 2009
 *
 * This program and the accompanying materials
 * are made available under the terms of the
 * Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at
 *
 * 		http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
 * OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * Contributors:
 * 	   IBM Corporation - initial API and implementation for JDT/DLTK
 *     Sean W. Quinn - initial adoption for use with PHP from various sources.
 */
package org.eclipse.php.internal.core;

import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.SocketException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.util.HashMap;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.dltk.core.IBuildpathEntry;
import org.eclipse.dltk.core.IMember;
import org.eclipse.dltk.core.WorkingCopyOwner;
import org.eclipse.dltk.internal.core.ModelElement;
import org.eclipse.dltk.internal.core.util.MementoTokenizer;
import org.eclipse.dltk.internal.core.util.Util;
import org.eclipse.php.core.IPHPElement;
import org.eclipse.php.core.IPHPModel;
import org.eclipse.php.core.IPHPModelStatus;
import org.eclipse.php.core.IPHPModelStatusConstants;
import org.eclipse.php.core.IPHPProject;
import org.eclipse.php.core.IProgramUnit;
import org.eclipse.php.core.IScriptFile;
import org.eclipse.php.core.PHPModelException;
import org.eclipse.php.internal.compiler.lookup.Binding;
import org.eclipse.php.internal.core.ast.nodes.ASTNode;
import org.eclipse.php.internal.core.ast.nodes.ProgramExt;
import org.eclipse.php.internal.core.ast.util.UtilExt;

/**
 * @author Sean W. Quinn (swquinn@gmail.com)
 */
@SuppressWarnings("restriction")
public abstract class PHPElement extends ModelElement implements IPHPElement {
	//	private static final QualifiedName PROJECT_JAVADOC= new QualifiedName(PHPCore.PLUGIN_ID, "project_javadoc_location"); //$NON-NLS-1$

	private static final byte[] CLOSING_DOUBLE_QUOTE = new byte[] { 34 };
	private static final byte[] CHARSET = new byte[] { 99, 104, 97, 114, 115, 101, 116, 61 };
	private static final byte[] CONTENT_TYPE = new byte[] { 34, 67, 111, 110, 116, 101, 110, 116, 45, 84, 121, 112,
			101, 34 };
	private static final byte[] CONTENT = new byte[] { 99, 111, 110, 116, 101, 110, 116, 61, 34 };

	public static final IPHPElement[] NO_ELEMENTS = (IPHPElement[]) ModelElement.NO_ELEMENTS;

	public static final char JEM_NAMESPACEFRAGMENTROOT = '/';
	public static final char JEM_NAMESPACEFRAGMENT = '<';

	/**
	 * Constructs a handle for a java element with the given parent element.
	 * 
	 * @param parent The parent of java element
	 * @exception IllegalArgumentException if the type is not one of the valid PHP element type constants
	 */
	protected PHPElement(final PHPElement parent) throws IllegalArgumentException {
		super(parent);
	}

	/**
	 * Returns the <code>ASTNode</code> that corresponds to this <code>PHPElement</code> or <code>null</code> if there
	 * is no corresponding node.
	 */
	public ASTNode findNode(ProgramExt ast) {
		return null; // works only inside a compilation unit
	}

	/**
	 * Generates the element infos for this element, its ancestors (if they are not opened) and its children (if it is
	 * an Openable). Puts the newly created element info in the given map.
	 */
	@Override
	protected abstract void generateInfos(Object info, HashMap newElements, IProgressMonitor pm)
			throws PHPModelException;

	/**
	 * @see IMember
	 */
	public IScriptFile getScriptFile() {
		return null;
	}

	/**
	 * @see IMember
	 */
	public IProgramUnit getProgramUnit() {
		return null;
	}

	/*
	 * Creates a PHP element handle from the given memento. The given token is the current delimiter indicating the type
	 * of the next token(s). The given working copy owner is used only for compilation unit handles.
	 */
	@Override
	public abstract IPHPElement getHandleFromMemento(String token, MementoTokenizer memento, WorkingCopyOwner owner);

	/*
	 * Creates a PHP element handle from the given memento. The given working copy owner is used only for compilation
	 * unit handles.
	 */
	@Override
	public IPHPElement getHandleFromMemento(MementoTokenizer memento, WorkingCopyOwner owner) {
		if (!memento.hasMoreTokens())
			return this;
		String token = memento.nextToken();
		return getHandleFromMemento(token, memento, owner);
	}

	/**
	 * @see IPHPElement
	 */
	@Override
	public String getHandleIdentifier() {
		return getHandleMemento();
	}

	/**
	 * Returns the <code>char</code> that marks the start of this handles contribution to a memento.
	 */
	@Override
	protected abstract char getHandleMementoDelimiter();

	/**
	 * @see IPHPElement
	 */
	public IPHPModel getPHPModel() {
		return (IPHPModel) getModel();
	}

	/**
	 * @see IPHPElement
	 */
	public IPHPProject getPHPProject() {
		return (IPHPProject) getScriptProject();
	}

	/*
	 * @see IPHPElement#getPrimaryElement()
	 */
	@Override
	public IPHPElement getPrimaryElement() {
		return getPrimaryElement(true);
	}

	/*
	 * Returns the primary element. If checkOwner, and the cu owner is primary, return this element.
	 */
	@Override
	public IPHPElement getPrimaryElement(boolean checkOwner) {
		return this;
	}

	public IResource getResource() {
		return resource();
	}

	public abstract IResource resource();

	/**
	 * Returns the element that is located at the given source position in this element. This is a helper method for
	 * <code>IProgramUnit#getElementAt</code>, and only works on compilation units and types. The position given is
	 * known to be within this element's source range already, and if no finer grained element is found at the position,
	 * this element is returned.
	 */
	@Override
	protected IPHPElement getSourceElementAt(int position) throws PHPModelException {
		return (IPHPElement) getSourceElementAt(position);
	}

	/**
	 * Returns the SourceMapper facility for this element, or <code>null</code> if this element does not have a
	 * SourceMapper.
	 */
	public SourceMapper getSourceMapper() {
		return ((PHPElement) getParent()).getSourceMapper();
	}

	/*
	 * (non-PHPdoc)
	 * @see org.eclipse.jdt.core.IPHPElement#getSchedulingRule()
	 */
	public ISchedulingRule getSchedulingRule() {
		IResource resource = resource();
		if (resource == null) {
			class NoResourceSchedulingRule implements ISchedulingRule {
				public IPath path;

				public NoResourceSchedulingRule(IPath path) {
					this.path = path;
				}

				public boolean contains(ISchedulingRule rule) {
					if (rule instanceof NoResourceSchedulingRule) {
						return this.path.isPrefixOf(((NoResourceSchedulingRule) rule).path);
					}
					else {
						return false;
					}
				}

				public boolean isConflicting(ISchedulingRule rule) {
					if (rule instanceof NoResourceSchedulingRule) {
						IPath otherPath = ((NoResourceSchedulingRule) rule).path;
						return this.path.isPrefixOf(otherPath) || otherPath.isPrefixOf(this.path);
					}
					else {
						return false;
					}
				}
			}
			return new NoResourceSchedulingRule(getPath());
		}
		else {
			return resource;
		}
	}

	protected PHPModelStatus newDoesNotExistStatus() {
		return new PHPModelStatus(IPHPModelStatusConstants.ELEMENT_DOES_NOT_EXIST, this);
	}

	/**
	 * Creates and returns a new PHP model exception for this element with the given status.
	 */
	public PHPModelException newPHPModelException(IStatus status) {
		if (status instanceof IPHPModelStatus)
			return new PHPModelException((IPHPModelStatus) status);
		else
			return new PHPModelException(
					new PHPModelStatus(status.getSeverity(), status.getCode(), status.getMessage()));
	}

	/**
	 */
	public String readableName() {
		return getElementName();
	}

	public PHPElement resolved(Binding binding) {
		return this;
	}

	public PHPElement unresolved() {
		return this;
	}

	/**
	 * @see org.eclipse.dltk.internal.core.ModelElement#getHandleMemento(java.lang.StringBuffer)
	 */
	@Override
	public void getHandleMemento(StringBuffer buff) {
		super.getHandleMemento(buff);
	}

	/**
	 * @return
	 * @throws PHPModelException
	 * @see org.eclipse.jdt.internal.core.JavaElement#getJavadocBaseLocation() for example
	 */
	@Override
	public IPHPElement getParent() {
		final IPHPElement phpElement = (IPHPElement) super.getParent();
		return phpElement;
	}

	/**
	 * @return
	 * @throws PHPModelException
	 * @see org.eclipse.jdt.internal.core.JavaElement#getJavadocBaseLocation() for example
	 */
	protected URL getPHPDocBaseLocation() throws PHPModelException {
		// FIXME: Need to implement this!
		throw new RuntimeException("PHPElement.getPHPDocBaseLocation() not yet implemented.");
	}

	/**
	 * @param jproject
	 * @param containerPath
	 * @param libPath
	 * @return
	 * @throws PHPModelException
	 * @see org.eclipse.jdt.internal.core.JavaElement#getRealClasspathEntry
	 */
	private static IBuildpathEntry getRealBuildpathEntry(IPHPProject jproject, IPath containerPath, IPath libPath)
			throws PHPModelException {
		throw new RuntimeException("PHPElement.getRealBuildpathEntry() not yet implemented");
	}

	/**
	 * @param entry
	 * @return
	 * @throws PHPModelException
	 * @see org.eclipse.jdt.internal.core.JavaElement#getLibraryJavadocLocation() for example
	 */
	protected static URL getLibraryPHPDocLocation(IBuildpathEntry entry) throws PHPModelException {
		// FIXME: Need to implement this!
		throw new RuntimeException("PHPElement.getLibraryPHPDocLocation() not yet implemented.");
	}

	/*
	 * @see IPHPElement#getAttachedPHPdoc(IProgressMonitor)
	 */
	public String getAttachedPHPdoc(IProgressMonitor monitor) throws PHPModelException {
		return null;
	}

	int getIndexOf(byte[] array, byte[] toBeFound, int start) {
		if (array == null || toBeFound == null)
			return -1;
		final int toBeFoundLength = toBeFound.length;
		final int arrayLength = array.length;
		if (arrayLength < toBeFoundLength)
			return -1;
		loop: for (int i = start, max = arrayLength - toBeFoundLength + 1; i < max; i++) {
			if (array[i] == toBeFound[0]) {
				for (int j = 1; j < toBeFoundLength; j++) {
					if (array[i + j] != toBeFound[j])
						continue loop;
				}
				return i;
			}
		}
		return -1;
	}

	/*
	 * We don't use getContentEncoding() on the URL connection, because it might leave open streams behind. See
	 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=117890
	 */
	protected String getURLContents(String docUrlValue) throws PHPModelException {
		InputStream stream = null;
		JarURLConnection connection2 = null;
		try {
			URL docUrl = new URL(docUrlValue);
			URLConnection connection = docUrl.openConnection();
			if (connection instanceof JarURLConnection) {
				connection2 = (JarURLConnection) connection;
				// https://bugs.eclipse.org/bugs/show_bug.cgi?id=156307
				connection.setUseCaches(false);
			}
			stream = new BufferedInputStream(connection.getInputStream());
			String encoding = connection.getContentEncoding();
			byte[] contents = UtilExt.getInputStreamAsByteArray(stream, connection
					.getContentLength());
			if (encoding == null) {
				int index = getIndexOf(contents, CONTENT_TYPE, 0);
				if (index != -1) {
					index = getIndexOf(contents, CONTENT, index);
					if (index != -1) {
						int offset = index + CONTENT.length;
						int index2 = getIndexOf(contents, CLOSING_DOUBLE_QUOTE, offset);
						if (index2 != -1) {
							final int charsetIndex = getIndexOf(contents, CHARSET, offset);
							if (charsetIndex != -1) {
								int start = charsetIndex + CHARSET.length;
								encoding = new String(contents, start, index2 - start,
										Util.UTF_8);
							}
						}
					}
				}
			}
			try {
				if (encoding == null) {
					encoding = getPHPProject().getProject().getDefaultCharset();
				}
			}
			catch (CoreException e) {
				// ignore
			}
			if (contents != null) {
				if (encoding != null) {
					return new String(contents, encoding);
				}
				else {
					// platform encoding is used
					return new String(contents);
				}
			}
		}
		catch (MalformedURLException e) {
			throw new PHPModelException(new PHPModelStatus(IPHPModelStatusConstants.CANNOT_RETRIEVE_ATTACHED_PHPDOC,
					this));
		}
		catch (FileNotFoundException e) {
			// ignore. see bug
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=120559
		}
		catch (SocketException e) {
			// ignore. see bug
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=247845
		}
		catch (UnknownHostException e) {
			// ignore. see bug
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=247845
		}
		catch (ProtocolException e) {
			// ignore. see bug
			// https://bugs.eclipse.org/bugs/show_bug.cgi?id=247845
		}
		catch (IOException e) {
			throw new PHPModelException(e, IPHPModelStatusConstants.IO_EXCEPTION);
		}
		finally {
			if (stream != null) {
				try {
					stream.close();
				}
				catch (IOException e) {
					// ignore
				}
			}
			if (connection2 != null) {
				try {
					connection2.getJarFile().close();
				}
				catch (IOException e) {
					// ignore
				}
				catch (IllegalStateException e) {
					/*
					 * ignore. Can happen in case the stream.close() did close the jar file see
					 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=140750
					 */
				}
			}
		}
		return null;
	}
}
